package net.oschina.bilbodai.common.beanutil.bean;

import net.oschina.bilbodai.common.beanutil.bean.builder.BeanInfoBuilder;
import net.oschina.bilbodai.common.beanutil.bean.builder.PropertyDumpInfoBuilder;
import net.oschina.bilbodai.common.beanutil.bean.code.IPropertyCodeApplier;
import net.oschina.bilbodai.common.beanutil.bean.code.PropertyCodeAppliers;
import net.oschina.bilbodai.common.beanutil.bean.core.PropertyInfo;
import net.oschina.bilbodai.common.beanutil.bean.core.PropertyInfoProcessor;
import net.oschina.bilbodai.common.beanutil.bean.type.NullType;
import net.oschina.bilbodai.common.beanutil.exception.PropertyDumpException;
import net.oschina.bilbodai.common.beanutil.util.StringUtil;
import net.oschina.bilbodai.common.beanutil.util.TypeUtil;
import org.objectweb.asm.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * @author daimao  Date: 16/4/21 Time: 下午1:17
 * @version $Id$
 */
class BeanAccessorProcessor {

    private static final Logger LOGGER = LoggerFactory.getLogger(BeanAccessorProcessor.class);

    public BeanInfo process(Class<?> srcType, Class<?> targetType, String profile) {

        final List<IPropertyCodeApplier> codeAppliers = new LinkedList<>();

        List<PropertyInfo> srcPropertyInfos = PropertyInfoProcessor.process(srcType, targetType, PropertyInfoProcessor.ACCEPT_GETTERS, profile);
        final Map<String, PropertyInfo> sourcePropertyInfoMap = mapPropertyInfos(srcPropertyInfos);
        final List<PropertyInfo> targetPropertyInfos = PropertyInfoProcessor.process(targetType, srcType, PropertyInfoProcessor.ACCEPT_SETTERS, profile);

        for (PropertyInfo targetInfo : targetPropertyInfos) {
            final String name = targetInfo.getName();
            final PropertyInfo sourceInfo = sourcePropertyInfoMap.get(name);
            //targetInfo must not null, but sourceInfo may be
            if (sourceInfo == null) {
                if (targetInfo.required())
                    throw new PropertyDumpException("TargetInfo doesn't found any matched SourceInfo, TargetInfo=" + targetInfo);
                LOGGER.info(targetInfo.getOwnerType().getSimpleName() + "." + targetInfo.getField().getName() + " was skipped dumping cause lacking of match SourceInfo, targetInfo=" + targetInfo);
                continue;
            }

            PropertyDumpInfoBuilder dumpInfoBuilder = new PropertyDumpInfoBuilder();

            dumpInfoBuilder.setIdName(targetInfo.getName());
            dumpInfoBuilder.setProfile(profile);
            dumpInfoBuilder.setSrcOwnerInternalName(Type.getInternalName(srcType));
            dumpInfoBuilder.setTargetOwnerInternalName(Type.getInternalName(targetType));
            boolean dumpOverride = (sourceInfo.isDumpOverride()) || targetInfo.isDumpOverride();

            //这里做一个小的调整，如果bean中每个字段都没有主动设置，并且target为原始类型，那么默认是覆盖字段的值
            boolean dumpOverrideCustomSet = (sourceInfo.isDumpOverrideCustomSet() || targetInfo.isDumpOverrideCustomSet());
            if (!dumpOverride && !dumpOverrideCustomSet && targetInfo.getFieldType().isPrimitive()) {
                dumpOverride = true;
            }

            dumpInfoBuilder.setDumpOverride(dumpOverride);
            if (!dumpOverride && targetInfo.getFieldType().isPrimitive()) {
                throw new PropertyDumpException("Primitive type must declare 'forceDump=true' or ignore dump, targetInfo=" + targetInfo);
            }


            dumpInfoBuilder.setAttrPathQueryInfo(getAttrPathQueryInfo(targetInfo, sourceInfo));

            dumpInfoBuilder.setSrcField(sourceInfo.getField());
            dumpInfoBuilder.setTargetField(targetInfo.getField());
            dumpInfoBuilder.setSrcFieldInternalName(Type.getInternalName(sourceInfo.getFieldType()));
            dumpInfoBuilder.setTargetFieldInternalName(Type.getInternalName(targetInfo.getFieldType()));
            dumpInfoBuilder.setSrcGetterName(sourceInfo.getGetterName());
            dumpInfoBuilder.setTargetGetterName(targetInfo.getGetterName());
            dumpInfoBuilder.setSrcSetterName(sourceInfo.getSetterName());
            dumpInfoBuilder.setTargetSetterName(targetInfo.getSetterName());
            dumpInfoBuilder.setSrcGetterDescriptor(sourceInfo.getGetterDescriptor());
            dumpInfoBuilder.setTargetGetterDescriptor(targetInfo.getGetterDescriptor());
            dumpInfoBuilder.setSrcSetterDescriptor(sourceInfo.getSetterDescriptor());
            dumpInfoBuilder.setTargetSetterDescriptor(targetInfo.getSetterDescriptor());

            dumpInfoBuilder.setCollectTypeImpl(getCollectTypeImpl(targetInfo, sourceInfo));
            dumpInfoBuilder.setSrcKeyType(getSrcKeyType(targetInfo, sourceInfo));
            dumpInfoBuilder.setTargetKeyType(getTargetKeyType(targetInfo, sourceInfo));
            Class<?> srcComponentType = getSrcComponentType(targetInfo, sourceInfo);
            dumpInfoBuilder.setSrcComponentType(srcComponentType);
            dumpInfoBuilder.setTargetComponentType(getTargetComponentType(targetInfo, sourceInfo));
            dumpInfoBuilder.setSorted(targetInfo.isSorted() || sourceInfo.isSorted());

            PropertyDumpType srcDumpType = getSrcDumpType(targetInfo, sourceInfo);
            PropertyDumpType targetDumpType = getTargetDumpType(targetInfo, sourceInfo);

            //from field
            Method mappedFieldGetterMethod = getMappedFieldGetterMethod(targetInfo, sourceInfo, srcComponentType);
            if (mappedFieldGetterMethod != null) {
                dumpInfoBuilder.setMapFieldOwnerInternalName(Type.getInternalName(srcComponentType));
                dumpInfoBuilder.setMapFieldInternalName(Type.getInternalName(mappedFieldGetterMethod.getReturnType()));
                dumpInfoBuilder.setMapFieldGetterName(mappedFieldGetterMethod.getName());
                dumpInfoBuilder.setMapFieldGetterDescriptor(Type.getMethodDescriptor(mappedFieldGetterMethod));
            }

            IPropertyCodeApplier codeApplier = PropertyCodeAppliers.getCodeAppliers(dumpInfoBuilder.createPropertyDumpInfo(), sourceInfo, targetInfo, srcDumpType, targetDumpType);
            if (codeApplier != null) {
                codeAppliers.add(codeApplier);
            }
        }

        return new BeanInfoBuilder()
                .setSrcInternalName(Type.getInternalName(srcType))
                .setTargetInternalName(Type.getInternalName(targetType))
                .setPropertyCodeAppliers(codeAppliers)
                .createBeanInfo();
    }

    private Method getMappedFieldGetterMethod(PropertyInfo targetInfo, PropertyInfo sourceInfo, Class<?> mapFieldOwnerType) {
        String mapFieldName = targetInfo.getMapFieldName();
        if (StringUtil.isEmpty(mapFieldName)) {
            mapFieldName = sourceInfo.getMapFieldName();
        }
        if (StringUtil.isEmpty(mapFieldName)) {
            return null;
        }
        return TypeUtil.getGetterMethod(mapFieldName, mapFieldOwnerType);
    }

    private PropertyDumpType getTargetDumpType(PropertyInfo targetInfo, PropertyInfo sourceInfo) {
        PropertyDumpType targetDumpType = targetInfo.getTargetDumpType();
        if (targetDumpType.isSet())
            return targetDumpType;
        if (sourceInfo != null) {
            return sourceInfo.getTargetDumpType();
        }
        return PropertyDumpType.NONE;
    }

    private PropertyDumpType getSrcDumpType(PropertyInfo targetInfo, PropertyInfo sourceInfo) {
        if (sourceInfo != null) {
            PropertyDumpType srcDumpType = sourceInfo.getSrcDumpType();
            if (srcDumpType.isSet())
                return srcDumpType;
        }
        return targetInfo.getSrcDumpType();
    }

    private Class<?> getSrcComponentType(PropertyInfo targetInfo, PropertyInfo sourceInfo) {
        if (sourceInfo != null) {
            Class<?> componentType = sourceInfo.getComponentType();
            if (!componentType.equals(NullType.class))
                return componentType;
            Class<?> setSrcComponentType = sourceInfo.getSetSrcComponentType();
            if (!setSrcComponentType.equals(NullType.class))
                return setSrcComponentType;
        }
        Class<?> setSrcComponentType = targetInfo.getSetSrcComponentType();
        if (!setSrcComponentType.equals(NullType.class))
            return setSrcComponentType;
        return targetInfo.getComponentType();
    }

    private Class<?> getTargetComponentType(PropertyInfo targetInfo, PropertyInfo sourceInfo) {
        Class<?> componentType = targetInfo.getComponentType();
        if (!componentType.equals(NullType.class))
            return componentType;
        Class<?> setTargetComponentType = targetInfo.getSetTargetComponentType();
        if (!setTargetComponentType.equals(NullType.class))
            return setTargetComponentType;
        if (sourceInfo != null) {
            setTargetComponentType = sourceInfo.getSetTargetComponentType();
            if (!setTargetComponentType.equals(NullType.class))
                return setTargetComponentType;
            componentType = sourceInfo.getComponentType();
            if (!componentType.equals(NullType.class))
                return componentType;
        }
        return NullType.class;
    }

    private Class<?> getSrcKeyType(PropertyInfo targetInfo, PropertyInfo sourceInfo) {
        if (sourceInfo != null) {
            Class<?> keyType = sourceInfo.getKeyType();
            if (!keyType.equals(NullType.class))
                return keyType;
            Class<?> settedSrcKeyType = sourceInfo.getSetSrcKeyType();
            if (settedSrcKeyType.equals(NullType.class))
                return settedSrcKeyType;
        }
        Class<?> settedSrcKeyType = targetInfo.getSetSrcKeyType();
        if (settedSrcKeyType.equals(NullType.class))
            return settedSrcKeyType;
        return targetInfo.getKeyType();
    }

    private Class<?> getTargetKeyType(PropertyInfo targetInfo, PropertyInfo sourceInfo) {
        Class<?> keyType = targetInfo.getKeyType();
        if (!keyType.equals(NullType.class))
            return keyType;
        Class<?> settedTargetKeyType = targetInfo.getSetTargetKeyType();
        if (!settedTargetKeyType.equals(NullType.class))
            return settedTargetKeyType;
        if (sourceInfo != null) {
            settedTargetKeyType = sourceInfo.getSetTargetKeyType();
            if (!settedTargetKeyType.equals(NullType.class))
                return settedTargetKeyType;
            keyType = sourceInfo.getKeyType();
            if (!keyType.equals(NullType.class))
                return keyType;
        }
        return NullType.class;
    }

    private Class<?> getCollectTypeImpl(PropertyInfo targetInfo, PropertyInfo sourceInfo) {
        if (!targetInfo.getCollectTypeImpl().equals(NullType.class)) {
            return targetInfo.getCollectTypeImpl();
        }
        if (sourceInfo != null)
            return sourceInfo.getCollectTypeImpl();
        return NullType.class;
    }

    private AttrPathQueryInfo getAttrPathQueryInfo(PropertyInfo targetInfo, PropertyInfo sourceInfo) {
        if (targetInfo.usingAttrPathQuery()) {
            Class<?> attrQueryType = targetInfo.getAttrQueryType();
            if (attrQueryType.equals(NullType.class))
                throw new IllegalArgumentException("attrQueryType must be specified when using attrQuery! targetInfo=" + targetInfo);
            return AttrPathQueryInfo.create(attrQueryType, targetInfo.getAttrQueryPath());
        }
        if (sourceInfo != null && sourceInfo.usingAttrPathQuery()) {
            Class<?> attrQueryType = sourceInfo.getAttrQueryType();
            if (attrQueryType.equals(NullType.class))
                throw new IllegalArgumentException("attrQueryType must be specified when using attrQuery! sourceInfo=" + sourceInfo);
            return AttrPathQueryInfo.create(attrQueryType, sourceInfo.getAttrQueryPath());
        }
        return null;
    }

    private Map<String, PropertyInfo> mapPropertyInfos(List<PropertyInfo> propertyInfos) {
        Map<String, PropertyInfo> propertyInfoMap = new HashMap<>(propertyInfos.size());
        for (PropertyInfo propertyInfo : propertyInfos) {
            String name = propertyInfo.getName();
            if (propertyInfoMap.containsKey(name)) {
                throw new PropertyDumpException("Duplicate Dump Name Found, Origin=" + propertyInfoMap.get(name) + ", Now=" + propertyInfo);
            }
            propertyInfoMap.put(name, propertyInfo);

        }
        return propertyInfoMap;
    }
}